﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;

namespace WindowsFormsApplication1
{
    public class NodeLayoutModel
    {
        public const float MIN_SCALE_FACTOR = 0.1f;
        public const float MAX_SCALE_FACTOR = 2.0f;
        public const float SCALE_DELTA = 0.1f;

        readonly Dictionary<ElementModel, LinkedListNode<ElementModel>> zOrderIndex = new Dictionary<ElementModel, LinkedListNode<ElementModel>>();
        readonly LinkedList<ElementModel> zOrderedElements = new LinkedList<ElementModel>();

        /// <summary>
        /// Координата точки представления, находящаяся по центру области отображения.
        /// </summary>
        public Point ViewPortPosition;

        /// <summary>
        /// Размер отображаемого пространства.
        /// </summary>
        public Size ViewPortSize;

        /// <summary>
        /// Ширина рамки области представления в пикселах.
        /// </summary>
        public int BorderWidth;

        /// <summary>
        /// Флаг, равный true, если нужно отображать сетку, иначе false.
        /// </summary>
        public bool DrawGrid;

        /// <summary>
        /// Размер ячейки сетки.
        /// </summary>
        public int GridSize;

        /// <summary>
        /// Текущий масштаб отображения.
        /// </summary>
        public float Scale;

        /// <summary>
        /// Прямоугольник, описывающий область выделения.
        /// </summary>
        public Rectangle? SelectionBox;

        public HashSet<ElementModel> SelectedElements { get; private set; }

        public NodeLayoutModel()
        {
            Scale = 1.0f;
            GridSize = 256;
            DrawGrid = true;
            BorderWidth = 1;
            ViewPortPosition = new Point(0, 0);
            SelectedElements = new HashSet<ElementModel>();

            AddElement(new object());
            AddElement(new object());
            AddElement(new object());
        }

        public void ZoomIn()
        {
            if (Scale < MAX_SCALE_FACTOR)
                Scale += SCALE_DELTA;
        }

        public void ZoomOut()
        {
            if (Scale > MIN_SCALE_FACTOR)
                Scale -= SCALE_DELTA;
        }

        public void AddElement(object tag)
        {
            var model = new ElementModel(tag) {Location = new Point(-400, -300), Size = new Size(200, 100)};
            zOrderIndex.Add(model, zOrderedElements.AddLast(model));
        }

        public void MoveToTop(ElementModel element)
        {
            var node = zOrderIndex[element];
            zOrderedElements.Remove(node);
            zOrderedElements.AddLast(node);
        }

        /// <summary>
        /// Возвращает перечисление видимых линий сетки для отображения в представлении.
        /// </summary>
        public IEnumerable<Line> GetVisibleGridLines()
        {
            Func<int, int> Adjust = i => i - i%GridSize;
            var viewTopLeft = ClientToView(new Point(BorderWidth, BorderWidth));
            var leftVertAxis = Adjust(viewTopLeft.X);
            var topHorzAxis = Adjust(viewTopLeft.Y);

            var viewBottomRight = ClientToView(new Point(ViewPortSize.Width - BorderWidth*2, ViewPortSize.Height - BorderWidth*2));
            var rightVertAxis = Adjust(viewBottomRight.X);
            var bottomHorzAxis = Adjust(viewBottomRight.Y);

            for (var x = leftVertAxis; x <= rightVertAxis; x += GridSize)
                yield return new Line(ViewToClient(new Point(x, viewTopLeft.Y)), ViewToClient(new Point(x, viewBottomRight.Y)));
            for (var y = topHorzAxis; y <= bottomHorzAxis; y += GridSize)
                yield return new Line(ViewToClient(new Point(viewTopLeft.X, y)), ViewToClient(new Point(viewBottomRight.X, y)));
        }

        /// <summary>
        /// Возвращает перечисление элементов, которые необходимо отобразить в представлении в порядке увеличение Z-порядка.
        /// </summary>
        public IEnumerable<ElementModel> GetVisibleElements()
        {
            var clientRect = ClientToView(new Rectangle(new Point(BorderWidth, BorderWidth), new Size(ViewPortSize.Width - BorderWidth*2, ViewPortSize.Height - BorderWidth*2)));
            return zOrderedElements.Where(e => clientRect.IntersectsWith(e.GetRectangle()));
        }

        /// <summary>
        /// Преобразует координаты точки области отображения координаты отображения на экране.
        /// </summary>
        /// <param name="p">Координаты точки.</param>
        public Point ViewToClient(Point p)
        {
            return new Point((int) (ViewPortSize.Width/2.0f + (p.X + ViewPortPosition.X)*Scale), (int) (ViewPortSize.Height/2.0 + (p.Y + ViewPortPosition.Y)*Scale));
        }

        /// <summary>
        /// Преобразует координаты точки контрола в координаты области отображения.
        /// </summary>
        /// <param name="p">Координаты точки.</param>
        public Point ClientToView(Point p)
        {
            return new Point((int) ((p.X - ViewPortSize.Width/2.0f)/Scale - ViewPortPosition.X), (int) ((p.Y - ViewPortSize.Height/2.0f)/Scale - ViewPortPosition.Y));
        }

        /// <summary>
        /// Преобразует координаты прямоугольника из глобальных координат в координаты области отображения.
        /// </summary>
        /// <param name="rect">Прямоугольник.</param>
        public Rectangle ViewToClient(Rectangle rect)
        {
            return new Rectangle(ViewToClient(rect.Location), new Size((int) (rect.Width*Scale), (int) (rect.Height*Scale)));
        }

        /// <summary>
        /// Преобразует координаты прямоугольника из координат области отображения в глобальные координаты.
        /// </summary>
        /// <param name="rect">Прямоугольник.</param>
        public Rectangle ClientToView(Rectangle rect)
        {
            return new Rectangle(ClientToView(rect.Location), new Size((int) (rect.Width/Scale), (int) (rect.Height/Scale)));
        }
    }

    public struct Line
    {
        public readonly Point Point1;
        public readonly Point Point2;

        public Line(Point point1, Point point2)
        {
            Point1 = point1;
            Point2 = point2;
        }
    }
}